import {Object3D} from '../core/Object3D.js'

class Audio extends Object3D {
  constructor(listener) {
    super()

    this.type = 'Audio'

    this.listener = listener
    this.context = listener.context

    this.gain = this.context.createGain()
    this.gain.connect(listener.getInput())

    this.autoplay = false

    this.buffer = null
    this.detune = 0
    this.loop = false
    this.loopStart = 0
    this.loopEnd = 0
    this.offset = 0
    this.duration = undefined
    this.playbackRate = 1
    this.isPlaying = false
    this.hasPlaybackControl = true
    this.source = null
    this.sourceType = 'empty'

    this._startedAt = 0
    this._progress = 0
    this._connected = false

    this.filters = []
  }

  getOutput() {
    return this.gain
  }

  setNodeSource(audioNode) {
    this.hasPlaybackControl = false
    this.sourceType = 'audioNode'
    this.source = audioNode
    this.connect()

    return this
  }

  setMediaElementSource(mediaElement) {
    this.hasPlaybackControl = false
    this.sourceType = 'mediaNode'
    this.source = this.context.createMediaElementSource(mediaElement)
    this.connect()

    return this
  }

  setMediaStreamSource(mediaStream) {
    this.hasPlaybackControl = false
    this.sourceType = 'mediaStreamNode'
    this.source = this.context.createMediaStreamSource(mediaStream)
    this.connect()

    return this
  }

  setBuffer(audioBuffer) {
    this.buffer = audioBuffer
    this.sourceType = 'buffer'

    if (this.autoplay) this.play()

    return this
  }

  play(delay) {
    if (delay === undefined) delay = 0

    if (this.isPlaying === true) {
      console.warn('THREE.Audio: Audio is already playing.')
      return
    }

    if (this.hasPlaybackControl === false) {
      console.warn('THREE.Audio: this Audio has no playback control.')
      return
    }

    this._startedAt = this.context.currentTime + delay

    const source = this.context.createBufferSource()
    source.buffer = this.buffer
    source.loop = this.loop
    source.loopStart = this.loopStart
    source.loopEnd = this.loopEnd
    source.onended = this.onEnded.bind(this)
    source.start(this._startedAt, this._progress + this.offset, this.duration)

    this.isPlaying = true

    this.source = source

    this.setDetune(this.detune)
    this.setPlaybackRate(this.playbackRate)

    return this.connect()
  }

  pause() {
    if (this.hasPlaybackControl === false) {
      console.warn('THREE.Audio: this Audio has no playback control.')
      return
    }

    if (this.isPlaying === true) {
      // update current progress

      this._progress += Math.max(this.context.currentTime - this._startedAt, 0) * this.playbackRate

      if (this.loop === true) {
        // ensure _progress does not exceed duration with looped audios

        this._progress = this._progress % (this.duration || this.buffer.duration)
      }

      this.source.stop()
      this.source.onended = null

      this.isPlaying = false
    }

    return this
  }

  stop() {
    if (this.hasPlaybackControl === false) {
      console.warn('THREE.Audio: this Audio has no playback control.')
      return
    }

    this._progress = 0

    this.source.stop()
    this.source.onended = null
    this.isPlaying = false

    return this
  }

  connect() {
    if (this.filters.length > 0) {
      this.source.connect(this.filters[0])

      for (let i = 1, l = this.filters.length; i < l; i++) {
        this.filters[i - 1].connect(this.filters[i])
      }

      this.filters[this.filters.length - 1].connect(this.getOutput())
    } else {
      this.source.connect(this.getOutput())
    }

    this._connected = true

    return this
  }

  disconnect() {
    if (this.filters.length > 0) {
      this.source.disconnect(this.filters[0])

      for (let i = 1, l = this.filters.length; i < l; i++) {
        this.filters[i - 1].disconnect(this.filters[i])
      }

      this.filters[this.filters.length - 1].disconnect(this.getOutput())
    } else {
      this.source.disconnect(this.getOutput())
    }

    this._connected = false

    return this
  }

  getFilters() {
    return this.filters
  }

  setFilters(value) {
    if (!value) value = []

    if (this._connected === true) {
      this.disconnect()
      this.filters = value
      this.connect()
    } else {
      this.filters = value
    }

    return this
  }

  setDetune(value) {
    this.detune = value

    if (this.source.detune === undefined) return // only set detune when available

    if (this.isPlaying === true) {
      this.source.detune.setTargetAtTime(this.detune, this.context.currentTime, 0.01)
    }

    return this
  }

  getDetune() {
    return this.detune
  }

  getFilter() {
    return this.getFilters()[0]
  }

  setFilter(filter) {
    return this.setFilters(filter ? [filter] : [])
  }

  setPlaybackRate(value) {
    if (this.hasPlaybackControl === false) {
      console.warn('THREE.Audio: this Audio has no playback control.')
      return
    }

    this.playbackRate = value

    if (this.isPlaying === true) {
      this.source.playbackRate.setTargetAtTime(this.playbackRate, this.context.currentTime, 0.01)
    }

    return this
  }

  getPlaybackRate() {
    return this.playbackRate
  }

  onEnded() {
    this.isPlaying = false
  }

  getLoop() {
    if (this.hasPlaybackControl === false) {
      console.warn('THREE.Audio: this Audio has no playback control.')
      return false
    }

    return this.loop
  }

  setLoop(value) {
    if (this.hasPlaybackControl === false) {
      console.warn('THREE.Audio: this Audio has no playback control.')
      return
    }

    this.loop = value

    if (this.isPlaying === true) {
      this.source.loop = this.loop
    }

    return this
  }

  setLoopStart(value) {
    this.loopStart = value

    return this
  }

  setLoopEnd(value) {
    this.loopEnd = value

    return this
  }

  getVolume() {
    return this.gain.gain.value
  }

  setVolume(value) {
    this.gain.gain.setTargetAtTime(value, this.context.currentTime, 0.01)

    return this
  }
}

export {Audio}
